﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Diagnostics;

using JavaSubstitutes;
using Sonar;

namespace Sprites
{
	public class Mario : Sprite
	{
		public static bool large = false;
		public static bool fire = false;
		public static int coins = 0;
        public static int lives = 3; //todo: why's game state being stored in the mario sprite class?
		public static String levelString = "none";

		public const int KEY_LEFT = 0;
		public const int KEY_RIGHT = 1;
		public const int KEY_DOWN = 2;
		public const int KEY_UP = 3;
		public const int KEY_JUMP = 4;
		public const int KEY_SPEED = 5;

		private static double GROUND_INERTIA = 0.89;
		private static double AIR_INERTIA = 0.89;

		public bool[] keys;
		private double runTime;
		public bool wasOnGround = false;
		public bool onGround = false;
		private bool mayJump = false;
		private bool ducking = false;
		private bool sliding = false;
		private int jumpTime = 0;
		private double xJumpSpeed;
		private double yJumpSpeed;
		private bool canShoot = false;

		int width = 4;
		public int height = 24;

		private LevelScene world;
		public int facing;
		private int powerUpTime = 0;

		public int xDeathPos, yDeathPos;

		public int deathTime = 0;
		public int winTime = 0;
		private int invulnerableTime = 0;

        private bool lastLarge;
        private bool lastFire;
        private bool newLarge;
        private bool newFire;

		public Sprite carried = null;
		private static Mario instance;

        private double debugLast = 0;
        public override double x
        {
            get { return base.x; }
            set 
            {
                base.x = value;

                if (Math.Abs(value - debugLast) > 1)
                {
                    debugLast = value;
                    //Debug.WriteLine(value);
                }
            }
        }

        public override double y
        {
            get { return base.y; }
            set { base.y = value; }
        }


		public Mario(LevelScene world)
		{
			Mario.instance = this;
			this.world = world;
			keys = Scene.keys;
			x = 0;
			y = 0;

			facing = 1;
			setLarge(Mario.large, Mario.fire);
		}


        //todo: why's game state being stored in the mario sprite class?
        public static void Reset()
        {
            large = false;
            fire = false;
            coins = 0;
            lives = 3;
            levelString = "none";
        }

		private void blink(bool on)
		{
			Mario.large = on ? newLarge : lastLarge;
			Mario.fire = on ? newFire : lastFire;

			if (large)
			{
				sheet = Art.mario;
				if (fire)
					sheet = Art.fireMario;

				xPicO = 16;
				yPicO = 31;
				wPic = hPic = 32;
			}
			else
			{
				sheet = Art.smallMario;

				xPicO = 8;
				yPicO = 15;
				wPic = hPic = 16;
			}

			calcPic();
		}

		void setLarge(bool large, bool fire)
		{
			if (fire) large = true;
			if (!large) fire = false;

			lastLarge = Mario.large;
			lastFire = Mario.fire;

			Mario.large = large;
			Mario.fire = fire;

			newLarge = Mario.large;
			newFire = Mario.fire;

			blink(true);
		}

		public override void move()
		{
			if (winTime > 0)
			{
				winTime++;

				xDelta = 0;
				yDelta = 0;
				return;
			}

			if (deathTime > 0)
			{
				deathTime++;
				if (deathTime < 11)
				{
					xDelta = 0;
					yDelta = 0;
				}
				else if (deathTime == 11)
				{
					yDelta = -15;
				}
				else
				{
					yDelta += 2;
				}
				x += xDelta;
				y += yDelta;
				return;
			}

			if (powerUpTime != 0)
			{
				if (powerUpTime > 0)
				{
					powerUpTime--;
					blink(((powerUpTime / 3) & 1) == 0);
				}
				else
				{
					powerUpTime++;
					blink(((-powerUpTime / 3) & 1) == 0);
				}

				if (powerUpTime == 0) world.paused = false;

				calcPic();
				return;
			}

			if (invulnerableTime > 0) invulnerableTime--;
			visible = ((invulnerableTime / 2) & 1) == 0;

			wasOnGround = onGround;
			double sideWaysSpeed = keys[KEY_SPEED] ? 1.2 : 0.6;

			if (onGround)
			{
				if (keys[KEY_DOWN] && large)
				{
					ducking = true;
				}
				else
				{
					ducking = false;
				}
			}

			if (xDelta > 2)
			{
				facing = 1;
			}
			if (xDelta < -2)
			{
				facing = -1;
			}

			if (keys[KEY_JUMP] || (jumpTime < 0 && !onGround && !sliding))
			{
				if (jumpTime < 0)
				{
					xDelta = xJumpSpeed;
					yDelta = -jumpTime * yJumpSpeed;
					jumpTime++;
				}
				else if (onGround && mayJump)
				{
					world.sound.play(Art.samples[Art.SAMPLE_MARIO_JUMP], this, 1, 1, 1);
					xJumpSpeed = 0;
					yJumpSpeed = -1.9;
					jumpTime = 7;
					yDelta = jumpTime * yJumpSpeed;
					onGround = false;
					sliding = false;
				}
				else if (sliding && mayJump)
				{
					world.sound.play(Art.samples[Art.SAMPLE_MARIO_JUMP], this, 1, 1, 1);
					xJumpSpeed = -facing * 6.0;
					yJumpSpeed = -2.0;
					jumpTime = -6;
					xDelta = xJumpSpeed;
					yDelta = -jumpTime * yJumpSpeed;
					onGround = false;
					sliding = false;
					facing = -facing;
				}
				else if (jumpTime > 0)
				{
					xDelta += xJumpSpeed;
					yDelta = jumpTime * yJumpSpeed;
					jumpTime--;
				}
			}
			else
			{
				jumpTime = 0;
			}

			if (keys[KEY_LEFT] && !ducking)
			{
				if (facing == 1) sliding = false;
				xDelta -= sideWaysSpeed;
				if (jumpTime >= 0) facing = -1;
			}

			if (keys[KEY_RIGHT] && !ducking)
			{
				if (facing == -1) sliding = false;
				xDelta += sideWaysSpeed;
				if (jumpTime >= 0) facing = 1;
			}

			if ((!keys[KEY_LEFT] && !keys[KEY_RIGHT]) || ducking || yDelta < 0 || onGround)
			{
				sliding = false;
			}

			if (keys[KEY_SPEED] && canShoot && Mario.fire && world.fireballsOnScreen < 2)
			{
				world.sound.play(Art.samples[Art.SAMPLE_MARIO_FIREBALL], this, 1, 1, 1);
				world.addSprite(new Fireball(world, x + facing * 6, y - 20, facing));
			}

			canShoot = !keys[KEY_SPEED];

			mayJump = (onGround || sliding) && !keys[KEY_JUMP];

			xFlipPic = facing == -1;

			runTime += (Math.Abs(xDelta)) + 5;
			if (Math.Abs(xDelta) < 0.5)
			{
				runTime = 0;
				xDelta = 0;
			}

			calcPic();

			if (sliding)
			{
                //show sparkles while sliding 
				for (int i = 0; i < 1; i++)
				{
					world.addSprite(new Sparkle((int)(x + RandomExtensions.random() * 4 - 2) + facing * 8, (int)(y + RandomExtensions.random() * 4) - 24, (double)(RandomExtensions.random() * 2 - 1), (double)RandomExtensions.random() * 1, 0, 1, 5));
				}
				yDelta *= 0.5;
			}

			onGround = false;
			move(xDelta, 0); //meliot: why isn't it moving x and y at the same time?
			move(0, yDelta);

            //meliot: don't allow mario to get too high on the screen?
			if (y > world.level.height * 16 + 16)
			{
				die();
			}

            //don't allow mario to go past the lhs of the screen?
			if (x < 0)
			{
				x = 0;
				xDelta = 0;
			}

			if (x > world.level.xExit * 16)
			{
				win();
			}

			if (x > world.level.width * 16)
			{
				x = world.level.width * 16;
				xDelta = 0;
			}

			yDelta *= 0.85;
			if (onGround)
			{
				xDelta *= GROUND_INERTIA;
			}
			else
			{
				xDelta *= AIR_INERTIA;
			}

			if (!onGround)
			{
				yDelta += 3;
			}

			if (carried != null)
			{
				carried.x = x + facing * 8;
				carried.y = y - 2;
				if (!keys[KEY_SPEED])
				{
					carried.release(this);
					carried = null;
				}
			}
		}

		private void calcPic()
		{
			int runFrame = 0;

			if (large)
			{
				runFrame = ((int)(runTime / 20)) % 4;
				if (runFrame == 3) runFrame = 1;
				if (carried == null && Math.Abs(xDelta) > 10) runFrame += 3;
				if (carried != null) runFrame += 10;
				if (!onGround)
				{
					if (carried != null) runFrame = 12;
					else if (Math.Abs(xDelta) > 10) runFrame = 7;
					else runFrame = 6;
				}
			}
			else
			{
				runFrame = ((int)(runTime / 20)) % 2;
				if (carried == null && Math.Abs(xDelta) > 10) runFrame += 2;
				if (carried != null) runFrame += 8;
				if (!onGround)
				{
					if (carried != null) runFrame = 9;
					else if (Math.Abs(xDelta) > 10) runFrame = 5;
					else runFrame = 4;
				}
			}

			if (onGround && ((facing == -1 && xDelta > 0) || (facing == 1 && xDelta < 0)))
			{
				if (xDelta > 1 || xDelta < -1) runFrame = large ? 9 : 7;

				if (xDelta > 3 || xDelta < -3)
				{
					for (int i = 0; i < 3; i++)
					{
						world.addSprite(new Sparkle((int)(x + RandomExtensions.random() * 8 - 4), (int)(y + RandomExtensions.random() * 4), (double)(RandomExtensions.random() * 2 - 1), (double)RandomExtensions.random() * -1, 0, 1, 5));
					}
				}
			}

			if (large)
			{
				if (ducking) runFrame = 14;
				height = ducking ? 12 : 24;
			}
			else
			{
				height = 12;
			}

			xPic = runFrame;
		}

		//returns whether or not mario was able to move. If it wasn't able to move, it likely go blocked by something
		private bool move(double xDelta, double yDelta)
		{
            //meliot: limit to 8?
			while (xDelta > 8)
			{
				if (!move(8, 0)) return false;
				xDelta -= 8;
			}

			while (xDelta < -8)
			{
				if (!move(-8, 0)) return false;
				xDelta += 8;
			}

			while (yDelta > 8)
			{
				if (!move(0, 8)) return false;
				yDelta -= 8;
			}

			while (yDelta < -8)
			{
				if (!move(0, -8)) return false;
				yDelta += 8;
			}

			int correctionFactorTest = 0;// 16; //meliot removeme debug only

			bool collide = false;
			if (yDelta > 0)
			{
				if (isBlocking(x+correctionFactorTest  + xDelta - width, y + yDelta, xDelta, 0)) collide = true;
				else if (isBlocking(x+correctionFactorTest  + xDelta + width, y + yDelta, xDelta, 0)) collide = true;
				else if (isBlocking(x+correctionFactorTest  + xDelta - width, y + yDelta + 1, xDelta, yDelta)) collide = true;
				else if (isBlocking(x+correctionFactorTest  + xDelta + width, y + yDelta + 1, xDelta, yDelta)) collide = true;
			}

			if (yDelta < 0)
			{
				if (isBlocking(x+correctionFactorTest  + xDelta, y + yDelta - height, xDelta, yDelta)) collide = true;
				else if (collide || isBlocking(x+correctionFactorTest  + xDelta - width, y + yDelta - height, xDelta, yDelta)) collide = true;
				else if (collide || isBlocking(x+correctionFactorTest  + xDelta + width, y + yDelta - height, xDelta, yDelta)) collide = true;
			}

			if (xDelta > 0)
			{
				sliding = true;				
				if (isBlocking(x+correctionFactorTest + xDelta + width, y + yDelta - height, xDelta, yDelta)) collide = true;
				else sliding = false;
				if (isBlocking(x+correctionFactorTest + xDelta + width, y + yDelta - height / 2, xDelta, yDelta)) collide = true;
				else sliding = false;
				if (isBlocking(x+correctionFactorTest + xDelta + width, y + yDelta, xDelta, yDelta)) collide = true;
				else sliding = false;

                //if (collide) Debug.WriteLine("Collision: " + (x + xDelta + width).ToString() + "," + (y + yDelta + width).ToString());
			}

			if (xDelta < 0)
			{
				sliding = true;
				if (isBlocking(x+correctionFactorTest  + xDelta - width, y + yDelta - height, xDelta, yDelta)) collide = true;
				else sliding = false;
				if (isBlocking(x+correctionFactorTest  + xDelta - width, y + yDelta - height / 2, xDelta, yDelta)) collide = true;
				else sliding = false;
				if (isBlocking(x+correctionFactorTest  + xDelta - width, y + yDelta, xDelta, yDelta)) collide = true;
				else sliding = false;
			}

			if (collide)
			{
				if (xDelta < 0)
				{
					x = (int)((x - width) / 16) * 16 + width;
					this.xDelta = 0;
				}
				if (xDelta > 0)
				{
					x = (int)((x + width) / 16 + 1) * 16 - width - 1;
					this.xDelta = 0;
                    //Debug.WriteLine("Set x to: " + x.ToString());
				}
				if (yDelta < 0)
				{
					y = (int)((y - height) / 16) * 16 + height;
					jumpTime = 0;
					this.yDelta = 0;
				}
				if (yDelta > 0)
				{
					y = (int)((y - 1) / 16 + 1) * 16 - 1; 
					onGround = true;
				}
				return false;
			}
			else
			{
                //meliot: cut down on excessive assigment- makes debugging collision and movement items less noisly
				if(xDelta!=0) x += xDelta;
				if(yDelta!=0) y += yDelta;
				return true;
			}
		}

		private bool isBlocking(double xPixel, double yPixel, double xa, double ya)
		{
			int tileXToMoveTo = (int)(xPixel / 16);
			int tileYToMoveTo = (int)(yPixel / 16);
			if (tileXToMoveTo == (int)(this.x / 16) && tileYToMoveTo == (int)(this.y / 16)) return false;

			bool blocking = world.level.isBlocking(tileXToMoveTo, tileYToMoveTo, xa, ya);

			byte block = world.level.getBlock(tileXToMoveTo, tileYToMoveTo);

			if (((Level.TILE_BEHAVIORS[block & 0xff]) & Level.BIT_PICKUPABLE) > 0)
			{
				Mario.getCoin();
				world.sound.play(Art.samples[Art.SAMPLE_GET_COIN], new FixedSoundSource(tileXToMoveTo * 16 + 8, tileYToMoveTo * 16 + 8), 1, 1, 1);
				world.level.setBlock(tileXToMoveTo, tileYToMoveTo, (byte)0);
				for (int xx = 0; xx < 2; xx++)
					for (int yy = 0; yy < 2; yy++)
						world.addSprite(new Sparkle(tileXToMoveTo * 16 + xx * 8 + (int)(RandomExtensions.random() * 8), tileYToMoveTo * 16 + yy * 8 + (int)(RandomExtensions.random() * 8), 0, 0, 0, 2, 5));
			}

			if (blocking && ya < 0)
			{
				world.bump(tileXToMoveTo, tileYToMoveTo, large);
			}

			return blocking;
		}

		public void stomp(Enemy enemy)
		{
			if (deathTime > 0 || world.paused) return;

			double targetY = enemy.y - enemy.height / 2;
			move(0, targetY - y);

			world.sound.play(Art.samples[Art.SAMPLE_MARIO_KICK], this, 1, 1, 1);
			xJumpSpeed = 0;
			yJumpSpeed = -1.9;
			jumpTime = 8;
			yDelta = jumpTime * yJumpSpeed;
			onGround = false;
			sliding = false;
			invulnerableTime = 1;
		}

		public void stomp(Shell shell)
		{
			if (deathTime > 0 || world.paused) return;

			if (keys[KEY_SPEED] && shell.facing == 0)
			{
				carried = shell;
				shell.carried = true;
			}
			else
			{
				double targetY = shell.y - shell.height / 2;
				move(0, targetY - y);

				world.sound.play(Art.samples[Art.SAMPLE_MARIO_KICK], this, 1, 1, 1);
				xJumpSpeed = 0;
				yJumpSpeed = -1.9;
				jumpTime = 8;
				yDelta = jumpTime * yJumpSpeed;
				onGround = false;
				sliding = false;
				invulnerableTime = 1;
			}
		}

		public void getHurt()
		{
			if (deathTime > 0 || world.paused) return;
			if (invulnerableTime > 0) return;

			if (large)
			{
				world.paused = true;
				powerUpTime = -3 * 6;
				world.sound.play(Art.samples[Art.SAMPLE_MARIO_POWER_DOWN], this, 1, 1, 1);
				if (fire)
				{
					world.mario.setLarge(true, false); //meliot: why's this calling the global instance instead of this instance?
				}
				else
				{
					world.mario.setLarge(false, false);
				}
				invulnerableTime = 32;
			}
			else
			{
				die();
			}
		}

		private void win()
		{
			xDeathPos = (int)x;
			yDeathPos = (int)y;
			world.paused = true;
			winTime = 1;
			Art.stopMusic();
			world.sound.play(Art.samples[Art.SAMPLE_LEVEL_EXIT], this, 1, 1, 1);
		}

		public void die()
		{
			xDeathPos = (int)x;
			yDeathPos = (int)y;
			world.paused = true;
			deathTime = 1;
			Art.stopMusic();
			world.sound.play(Art.samples[Art.SAMPLE_MARIO_DEATH], this, 1, 1, 1);
		}


		public void getFlower()
		{
			if (deathTime > 0 || world.paused) return;

			if (!fire)
			{
				world.paused = true;
				powerUpTime = 3 * 6;
				world.sound.play(Art.samples[Art.SAMPLE_MARIO_POWER_UP], this, 1, 1, 1);
				world.mario.setLarge(true, true);
			}
			else
			{
				Mario.getCoin();
				world.sound.play(Art.samples[Art.SAMPLE_GET_COIN], this, 1, 1, 1);
			}
		}

		public void getMushroom()
		{
			if (deathTime > 0 || world.paused) return;

			if (!large)
			{
				world.paused = true;
				powerUpTime = 3 * 6;
				world.sound.play(Art.samples[Art.SAMPLE_MARIO_POWER_UP], this, 1, 1, 1);
				world.mario.setLarge(true, false);
			}
			else
			{
				Mario.getCoin();
				world.sound.play(Art.samples[Art.SAMPLE_GET_COIN], this, 1, 1, 1);
			}
		}

		public void kick(Shell shell)
		{
			if (deathTime > 0 || world.paused) return;

			if (keys[KEY_SPEED])
			{
				carried = shell;
				shell.carried = true;
			}
			else
			{
				world.sound.play(Art.samples[Art.SAMPLE_MARIO_KICK], this, 1, 1, 1);
				invulnerableTime = 1;
			}
		}

		public void stomp(BulletBill bill)
		{
			if (deathTime > 0 || world.paused) return;

			double targetY = bill.y - bill.height / 2;
			move(0, targetY - y);

			world.sound.play(Art.samples[Art.SAMPLE_MARIO_KICK], this, 1, 1, 1);
			xJumpSpeed = 0;
			yJumpSpeed = -1.9;
			jumpTime = 8;
			yDelta = jumpTime * yJumpSpeed;
			onGround = false;
			sliding = false;
			invulnerableTime = 1;
		}

		public byte getKeyMask()
		{
			int mask = 0;
			for (int i = 0; i < 7; i++)
			{
				if (keys[i]) mask |= (1 << i);
			}
			return (byte)mask;
		}

		public void setKeys(byte mask)
		{
			for (int i = 0; i < 7; i++)
			{
				keys[i] = (mask & (1 << i)) > 0;
			}
		}

		public static void get1Up()
		{
			instance.world.sound.play(Art.samples[Art.SAMPLE_MARIO_1UP], instance, 1, 1, 1);
			lives++;
			if (lives == 99)
			{
				lives = 99;
			}
		}

		public static void getCoin()
		{
			coins++;
			if (coins == 100)
			{
				coins = 0;
				get1Up();
			}
		}
	}
}